package conMgr.data;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import conMgr.model.Status;
import conMgr.model.UserType;
import conMgr.prog.ErrorProvider;

public class XmlManager extends ErrorProvider
{
	/**
	 * The format to use for parsing dates from XML.
	 */
	private final DateFormat dateFormat;
	
	/**
	 * Initializes a new XML manager.
	 */
	public XmlManager()
	{
		dateFormat = new SimpleDateFormat("MM/dd/yyyy");
	}
	
	/**
	 * Appends a new child element to the given element and returns it.
	 * @param parent The parent to append to.
	 * @param name The name of the new node.
	 * @return The newly created child node.
	 */
	public Element appendNode(final Element parent, final String name)
	{
		final Element child = parent.getOwnerDocument().createElement(name);
		parent.appendChild(child);
		return child;
	}
	
	/**
	 * Appends a new child attribute to the given element.
	 * @param parent The parent to append to.
	 * @param name The name of the new attribute.
	 * @param value The value of the new attribute.
	 */
	public void appendValue
	(
		final Element parent,
		final String name,
		final Date value
	)
	{
		appendValue(parent, name, dateFormat.format(value));
	}
	
	/**
	 * Appends a new child attribute to the given element.
	 * @param parent The parent to append to.
	 * @param name The name of the new attribute.
	 * @param value The value of the new attribute.
	 */
	public void appendValue
	(
		final Element parent,
		final String name,
		final Integer value
	)
	{
		appendValue(parent, name, value.toString());
	}
	
	/**
	 * Appends a new child attribute to the given element.
	 * @param parent The parent to append to.
	 * @param name The name of the new attribute.
	 * @param value The value of the new attribute.
	 */
	public void appendValue
	(
		final Element parent,
		final String name,
		final List<Integer> value
	)
	{
		final StringBuffer string = new StringBuffer();
		
		if (value.size() > 0)
			string.append(value.get(0));
		
		for (int i = 1; i < value.size(); i++)
		{
			string.append(',');
			string.append(value.get(i));
		}
		
		appendValue(parent, name, string.toString());
	}
	
	/**
	 * Appends a new child attribute to the given element.
	 * @param parent The parent to append to.
	 * @param name The name of the new attribute.
	 * @param value The value of the new attribute.
	 */
	public void appendValue
	(
		final Element parent,
		final String name,
		final String value
	)
	{
		final Attr attribute = parent.getOwnerDocument().createAttribute(name);
		attribute.setValue(value);
		parent.setAttributeNode(attribute);
	}
	
	/**
	 * Appends a new child attribute to the given element.
	 * @param parent The parent to append to.
	 * @param name The name of the new attribute.
	 * @param value The value of the new attribute.
	 * @param map A map of entities (keys) to entity IDs (values).
	 * @return A flag indicating if everything was successful.
	 */
	public <T> boolean appendValue
	(
		final Element parent,
		final String name,
		final T value,
		final Map<T, Integer> map
	)
	{
		final Integer id = map.get(value);
		
		if (id == null)
		{
			error("Internal error.");
			return false;
		}
		
		appendValue(parent, name, id);
		return true;
	}
	
	/**
	 * Appends a new child attribute to the given element.
	 * @param parent The parent to append to.
	 * @param name The name of the new attribute.
	 * @param value The value of the new attribute.
	 */
	public <T extends Enum<T>> void appendValue
	(
		final Element parent,
		final String name,
		final Enum<T> value
	)
	{
		appendValue(parent, name, value.toString());
	}
	
	/**
	 * Gets the date value of the given attribute name.
	 * @param element The element whose attributes to examine.
	 * @param name The name of the attribute.
	 * @param critical True if an error should be thrown if the attribute
	 * is missing or null; otherwise, false.
	 * @return The date value of the given attribute name or null if the attribute
	 * cannot be found.
	 */
	public Date getDate
	(
		final Element element,
		final String name,
		final boolean critical
	)
	{
		final String value = getString(element, name, critical);
		if (value == null) return null;
		
		try
		{
			return dateFormat.parse(value);
		}
		catch (final ParseException e)
		{
			if (critical)
				error
				(
					"Cannot parse '" + name + "' value '" + value +
					"' into a date.",
					e
				);
		}
		
		return null;
	}
	
	/**
	 * Gets a strongly-typed list of elements matching a given name.
	 * @param ancestor The XML element to get data from.
	 * @param containerName The element name of the container.
	 * @param elementName The name of the child elements to return.
	 * @return A strongly-typed list of elements matching a given name.
	 */
	public List<Element> getElements
	(
		final Element ancestor,
		final String containerName,
		final String elementName
	)
	{
		final List<Element> elements = new LinkedList<Element>();
		final NodeList container = ancestor.getElementsByTagName(containerName);
		
		if (container.getLength() == 0)
		{
			error("Found no <" + containerName + "> element in XML file. Cannot load data.");
			return null;
		}
		
		if (container.getLength() > 1)
			error("Found multiple <" + containerName + "> elements in XML file. Only loading the first one...");
		
		final NodeList list = container.item(0).getChildNodes();
		
		for (int i = 0; i < list.getLength(); i++)
		{
			final Node node = list.item(i);
			
			if (node.getNodeType() != Node.ELEMENT_NODE)
				continue;
			
			final Element element = (Element)node;
			
			if (!element.getNodeName().equals(elementName))
			{
				error("Found an unknown element in <" + containerName + "> in XML file.");
				return null;
			}
			
			elements.add((Element)node);
		}
		
		return elements;
	}
	
	/**
	 * Gets the entity value of the given attribute name.
	 * @param element The element whose attributes to examine.
	 * @param map A map of entity IDs (keys) to entities (values).
	 * @param name The name of the attribute.
	 * @param critical True if an error should be thrown if the attribute
	 * is missing or null; otherwise, false.
	 * @return The entity value of the given attribute name or null if the
	 * attribute cannot be found.
	 */
	public <T> T getEntity
	(
		final Element element,
		final Map<Integer, T> map,
		final String name,
		final boolean critical
	)
	{
		final Integer id = getInt(element, name, critical);
		if (id == null) return null;
		
		final T entity = map.get(id);
		
		if (entity == null)
		{
			if (critical)
				error("Cannot find entity with ID '" + id + "'.");
			
			return null;
		}
		
		return entity;
	}
	
	/**
	 * Gets the integer value of the given attribute name.
	 * @param element The element whose attributes to examine.
	 * @param name The name of the attribute.
	 * @param critical True if an error should be thrown if the attribute
	 * is missing or null; otherwise, false.
	 * @return The integer value of the given attribute name or null if the
	 * attribute cannot be found.
	 */
	public Integer getInt
	(
		final Element element,
		final String name,
		final boolean critical
	)
	{
		final String value = getString(element, name, critical);
		if (value == null) return null;
		
		try
		{
			return Integer.valueOf(value);
		}
		catch (final NumberFormatException e)
		{
			if (critical)
				error
				(
					"Cannot parse '" + name + "' value '" + value +
					"' into an integer.",
					e
				);
		}
		
		return null;
	}
	
	/**
	 * Gets the integer array value of the given attribute name.
	 * @param element The element whose attributes to examine.
	 * @param name The name of the attribute.
	 * @param critical True if an error should be thrown if the attribute
	 * is missing or null; otherwise, false.
	 * @return The integer array value of the given attribute name or null if
	 * the attribute cannot be found.
	 */
	public List<Integer> getIntArray
	(
		final Element element,
		final String name,
		final boolean critical
	)
	{
		final String value = getString(element, name, critical);
		if (value == null) return null;
		
		final String[] chunks = value.split(",");
		final List<Integer> list = new LinkedList<Integer>();
		
		for (String chunk : chunks)
		{
			try
			{
				list.add(Integer.valueOf(chunk));
			}
			catch (final NumberFormatException e)
			{
				if (critical)
					error
					(
						"Cannot parse '" + name + "' value '" + chunk +
						"' into an integer.",
						e
					);
				
				return null;
			}
		}
		
		return list;
	}
	
	/**
	 * Gets the status value of the given attribute name.
	 * @param element The element whose attributes to examine.
	 * @param name The name of the attribute.
	 * @param critical True if an error should be thrown if the attribute
	 * is missing or null; otherwise, false.
	 * @return The status value of the given attribute name or null if the
	 * attribute cannot be found.
	 */
	public Status getStatus
	(
		final Element element,
		final String name,
		final boolean critical
	)
	{
		final String value = getString(element, name, critical);
		if (value == null) return null;
		
		try
		{
			return Status.valueOf(value);
		}
		catch (final IllegalArgumentException e)
		{
			if (critical)
				error("Cannot parse a Status from '" + value + "'.", e);
		}
		
		return null;
	}
	
	/**
	 * Gets the string value of the given attribute name.
	 * @param element The element whose attributes to examine.
	 * @param name The name of the attribute.
	 * @param critical True if an error should be thrown if the attribute
	 * is missing or null; otherwise, false.
	 * @return The string value of the given attribute name or null if the
	 * attribute cannot be found.
	 */
	public String getString
	(
		final Element element,
		final String name,
		final boolean critical
	)
	{
		final Attr attribute = element.getAttributeNode(name);
		
		if (attribute == null || attribute.getValue() == null)
		{
			if (critical)
				error
				(
					"Cannot find '" + name + "' attribute on '" +
					element.getNodeName() + "' node."
				);
			
			return null;
		}
		
		return attribute.getValue();
	}
	
	/**
	 * Gets the user type value of the given attribute name.
	 * @param element The element whose attributes to examine.
	 * @param name The name of the attribute.
	 * @param critical True if an error should be thrown if the attribute
	 * is missing or null; otherwise, false.
	 * @return The user type value of the given attribute name or null if the
	 * attribute cannot be found.
	 */
	public UserType getType
	(
		final Element element,
		final String name,
		final boolean critical
	)
	{
		final String value = getString(element, name, critical);
		if (value == null) return null;
		
		try
		{
			return UserType.valueOf(value);
		}
		catch (final IllegalArgumentException e)
		{
			if (critical)
				error("Cannot parse a UserType from '" + value + "'.", e);
		}
		
		return null;
	}
}
